27. 开发Web应用

Spring Boot非常适合开发web应用程序。你可以使用内嵌的Tomcat,Jetty或Undertow轻轻松松地创建一个HTTP服务器。大多数的web应用都可以使用spring-boot-starter-web模块进行快速搭建和运行。

如果没有开发过Spring Boot web应用,可以参考Getting started章节的"Hello World!"示例。

27.1. Spring Web MVC框架

Spring Web MVC框架(通常简称为"Spring MVC")是一个富“模型,视图,控制器”web框架, 允许用户创建特定的@Controller@RestController beans来处理传入的HTTP请求,通过@RequestMapping注解可以将控制器中的方法映射到相应的HTTP请求。

示例:

@RestController
@RequestMapping(value="/users")
public class MyRestController {

    @RequestMapping(value="/{user}", method=RequestMethod.GET)
    public User getUser(@PathVariable Long user) {
        // ...
    }

    @RequestMapping(value="/{user}/customers", method=RequestMethod.GET)
    List<Customer> getUserCustomers(@PathVariable Long user) {
        // ...
    }

    @RequestMapping(value="/{user}", method=RequestMethod.DELETE)
    public User deleteUser(@PathVariable Long user) {
        // ...
    }
}

Spring MVC是Spring框架的核心部分,详细信息可以参考reference documentationspring.io/guides也有一些可用的指导覆盖Spring MVC。

27.1.1. Spring MVC自动配置

Spring Boot为Spring MVC提供的auto-configuration适用于大多数应用,并在Spring默认功能上添加了以下特性:

  1. 引入ContentNegotiatingViewResolverBeanNameViewResolver beans。
  2. 对静态资源的支持,包括对WebJars的支持。
  3. 自动注册ConverterGenericConverterFormatter beans。
  4. HttpMessageConverters的支持。
  5. 自动注册MessageCodeResolver
  6. 对静态index.html的支持。
  7. 对自定义Favicon的支持。
  8. 自动使用ConfigurableWebBindingInitializer bean。

如果保留Spring Boot MVC特性,你只需添加其他的MVC配置(拦截器,格式化处理器,视图控制器等)。你可以添加自己的WebMvcConfigurerAdapter类型的@Configuration类,而不需要注解@EnableWebMvc。如果希望使用自定义的RequestMappingHandlerMappingRequestMappingHandlerAdapter,或ExceptionHandlerExceptionResolver,你可以声明一个WebMvcRegistrationsAdapter实例提供这些组件。

如果想全面控制Spring MVC,你可以添加自己的@Configuration,并使用@EnableWebMvc注解。

27.1.2. HttpMessageConverters

Spring MVC使用HttpMessageConverter接口转换HTTP请求和响应,合适的默认配置可以开箱即用,例如对象自动转换为JSON(使用Jackson库)或XML(如果Jackson XML扩展可用,否则使用JAXB),字符串默认使用UTF-8编码。

可以使用Spring Boot的HttpMessageConverters类添加或自定义转换类:

import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.*;

@Configuration
public class MyConfiguration {

    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = ...
        HttpMessageConverter<?> another = ...
        return new HttpMessageConverters(additional, another);
    }
}

上下文中出现的所有HttpMessageConverter bean都将添加到converters列表,你可以通过这种方式覆盖默认的转换器列表(converters)。

27.1.3 自定义JSON序列化器和反序列化器

如果使用Jackson序列化,反序列化JSON数据,你可能想编写自己的JsonSerializerJsonDeserializer类。自定义序列化器(serializers)通常通过Module注册到Jackson,但Spring Boot提供了@JsonComponent注解这一替代方式,它能轻松的将序列化器注册为Spring Beans。

27.1.4 MessageCodesResolver

Spring MVC有一个实现策略,用于从绑定的errors产生用来渲染错误信息的错误码:MessageCodesResolver。Spring Boot会自动为你创建该实现,只要设置spring.mvc.message-codes-resolver.format属性为PREFIX_ERROR_CODEPOSTFIX_ERROR_CODE(具体查看DefaultMessageCodesResolver.Format枚举值)。

27.1.5 静态内容

默认情况下,Spring Boot从classpath下的/static/public/resources/META-INF/resources)文件夹,或从ServletContext根目录提供静态内容。这是通过Spring MVC的ResourceHttpRequestHandler实现的,你可以自定义WebMvcConfigurerAdapter并覆写addResourceHandlers方法来改变该行为(加载静态文件)。

在单机web应用中,容器会启动默认的servlet,并用它加载ServletContext根目录下的内容以响应那些Spring不处理的请求。大多数情况下这都不会发生(除非你修改默认的MVC配置),因为Spring总能够通过DispatcherServlet处理这些请求。

你可以设置spring.resources.staticLocations属性自定义静态资源的位置(配置一系列目录位置代替默认的值),如果你这样做,默认的欢迎页面将从自定义位置加载,所以只要这些路径中的任何地方有一个index.html,它都会成为应用的主页。

此外,除了上述标准的静态资源位置,有个例外情况是Webjars内容。任何在/webjars/**路径下的资源都将从jar文件中提供,只要它们以Webjars的格式打包。

如果你的应用将被打包成jar,那就不要使用src/main/webapp文件夹。尽管该文件夹是通常的标准格式,但它仅在打包成war的情况下起作用,在打包成jar时,多数构建工具都会默认忽略它。

Spring Boot也支持Spring MVC提供的高级资源处理特性,可用于清除缓存的静态资源或对WebJar使用版本无感知的URLs。

如果想使用针对WebJars版本无感知的URLs(version agnostic),只需要添加webjars-locator依赖,然后声明你的Webjar。以jQuery为例,"/webjars/jquery/dist/jquery.min.js"实际为"/webjars/jquery/x.y.z/dist/jquery.min.js"x.y.z为Webjar的版本。

如果使用JBoss,你需要声明webjars-locator-jboss-vfs依赖而不是webjars-locator,否则所有的Webjars将解析为404

以下的配置为所有的静态资源提供一种缓存清除(cache busting)方案,实际上是将内容hash添加到URLs中,比如<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

实现该功能的是ResourceUrlEncodingFilter,它在模板运行期会重写资源链接,Thymeleaf,Velocity和FreeMarker会自动配置该filter,JSP需要手动配置。其他模板引擎还没自动支持,不过你可以使用ResourceUrlProvider自定义模块宏或帮助类。

当使用比如JavaScript模块加载器动态加载资源时,重命名文件是不行的,这也是提供其他策略并能结合使用的原因。下面是一个"fixed"策略,在URL中添加一个静态version字符串而不需要改变文件名:

spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/lib/
spring.resources.chain.strategy.fixed.version=v12

使用以上策略,JavaScript模块加载器加载"/js/lib/"下的文件时会使用一个固定的版本策略"/v12/js/lib/mymodule.js",其他资源仍旧使用内容hash的方式<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>。查看ResourceProperties获取更多支持的选项。

该特性在一个专门的博文和Spring框架参考文档中有透彻描述。

27.1.6 ConfigurableWebBindingInitializer

Spring MVC使用WebBindingInitializer为每个特殊的请求初始化相应的WebDataBinder,如果你创建自己的ConfigurableWebBindingInitializer @Bean,Spring Boot会自动配置Spring MVC使用它。

27.1.7 模板引擎

正如REST web服务,你也可以使用Spring MVC提供动态HTML内容。Spring MVC支持各种各样的模板技术,包括Velocity, FreeMarker和JSPs,很多其他的模板引擎也提供它们自己的Spring MVC集成。

Spring Boot为以下的模板引擎提供自动配置支持:

  1. FreeMarker
  2. Groovy
  3. Thymeleaf
  4. Velocity(1.4已不再支持)
  5. Mustache

:由于在内嵌servlet容器中使用JSPs存在一些已知的限制,所以建议尽量不使用它们。

使用以上引擎中的任何一种,并采用默认配置,则模块会从src/main/resources/templates自动加载。

:IntelliJ IDEA根据你运行应用的方式会对classpath进行不同的排序。在IDE里通过main方法运行应用,跟从Maven,或Gradle,或打包好的jar中运行相比会导致不同的顺序,这可能导致Spring Boot不能从classpath下成功地找到模板。如果遇到这个问题,你可以在IDE里重新对classpath进行排序,将模块的类和资源放到第一位。或者,你可以配置模块的前缀为classpath*:/templates/,这样会查找classpath下的所有模板目录。

27.1.8 错误处理

Spring Boot默认提供一个/error映射用来以合适的方式处理所有的错误,并将它注册为servlet容器中全局的 错误页面。对于机器客户端(相对于浏览器而言,浏览器偏重于人的行为),它会产生一个具有详细错误,HTTP状态,异常信息的JSON响应。对于浏览器客户端,它会产生一个白色标签样式(whitelabel)的错误视图,该视图将以HTML格式显示同样的数据(可以添加一个解析为'error'的View来自定义它)。为了完全替换默认的行为,你可以实现ErrorController,并注册一个该类型的bean定义,或简单地添加一个ErrorAttributes类型的bean以使用现存的机制,只是替换显示的内容。

BasicErrorController可以作为自定义ErrorController的基类,如果你想添加对新context type的处理(默认处理text/html),这会很有帮助。你只需要继承BasicErrorController,添加一个public方法,并注解带有produces属性的@RequestMapping,然后创建该新类型的bean。

你也可以定义一个@ControllerAdvice去自定义某个特殊controller或exception类型的JSON文档:

@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler(YourException.class)
    @ResponseBody
    ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

}

在以上示例中,如果跟FooController相同package的某个controller抛出YourException,一个CustomerErrorType类型的POJO的json展示将代替ErrorAttributes展示。

自定义错误页面

如果想为某个给定的状态码展示一个自定义的HTML错误页面,你需要将文件添加到/error文件夹下。错误页面既可以是静态HTML(比如,任何静态资源文件夹下添加的),也可以是使用模板构建的,文件名必须是明确的状态码或一系列标签。

例如,映射404到一个静态HTML文件,你的目录结构可能如下:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

使用FreeMarker模板映射所有5xx错误,你需要如下的目录结构:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftl
             +- <other templates>

对于更复杂的映射,你可以添加实现ErrorViewResolver接口的beans:

public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request,
            HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        return ...
    }

}

你也可以使用Spring MVC特性,比如@ExceptionHandler方法@ControllerAdviceErrorController将处理所有未处理的异常。

映射Spring MVC以外的错误页面

对于不使用Spring MVC的应用,你可以通过ErrorPageRegistrar接口直接注册ErrorPages。该抽象直接工作于底层内嵌servlet容器,即使你没有Spring MVC的DispatcherServlet,它们仍旧可以工作。

@Bean
public ErrorPageRegistrar errorPageRegistrar(){
    return new MyErrorPageRegistrar();
}

// ...

private static class MyErrorPageRegistrar implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }

}

注.如果你注册一个ErrorPage,该页面需要被一个Filter处理(在一些非Spring web框架中很常见,比如Jersey,Wicket),那么该Filter需要明确注册为一个ERROR分发器(dispatcher),例如:

@Bean
public FilterRegistrationBean myFilter() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new MyFilter());
    ...
    registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
    return registration;
}

(默认的FilterRegistrationBean不包含ERROR dispatcher类型)。

WebSphere应用服务器的错误处理

当部署到一个servlet容器时,Spring Boot通过它的错误页面过滤器将带有错误状态的请求转发到恰当的错误页面。request只有在response还没提交时才能转发(forwarded)到正确的错误页面,而WebSphere应用服务器8.0及后续版本默认情况会在servlet方法成功执行后提交response,你需要设置com.ibm.ws.webcontainer.invokeFlushAfterService属性为false来关闭该行为。

27.1.9 Spring HATEOAS

如果正在开发基于超媒体的RESTful API,你可能需要Spring HATEOAS,而Spring Boot会为其提供自动配置,这在大多数应用中都运作良好。 自动配置取代了@EnableHypermediaSupport,只需注册一定数量的beans就能轻松构建基于超媒体的应用,这些beans包括LinkDiscoverers(客户端支持),ObjectMapper(用于将响应编排为想要的形式)。ObjectMapper可以根据spring.jackson.*属性或Jackson2ObjectMapperBuilder bean进行自定义。

通过注解@EnableHypermediaSupport,你可以控制Spring HATEOAS的配置,但这会禁用上述ObjectMapper的自定义功能。

27.1.10 CORS支持

跨域资源共享(CORS)是一个大多数浏览器都实现了的W3C标准,它允许你以灵活的方式指定跨域请求如何被授权,而不是采用那些不安全,性能低的方式,比如IFRAME或JSONP。

从4.2版本开始,Spring MVC对CORS提供开箱即用的支持。不用添加任何特殊配置,只需要在Spring Boot应用的controller方法上注解@CrossOrigin,并添加CORS配置。通过注册一个自定义addCorsMappings(CorsRegistry)方法的WebMvcConfigurer bean可以指定全局CORS配置

@Configuration
public class MyConfiguration {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }
        };
    }
}

27.2 JAX-RS和Jersey

如果你更喜欢JAX-RS为REST端点提供的编程模型,可以使用相应的实现代替Spring MVC。如果将Jersey 1.x和Apache CXF的ServletFilter注册到应用上下文中,那它们可以很好的工作。Spring对Jersey 2.x有一些原生支持,所以在Spring Boot中也为它提供了自动配置及一个starter。

想要使用Jersey 2.x,只需添加spring-boot-starter-jersey依赖,然后创建一个ResourceConfig类型的@Bean,用于注册所有的端点(endpoints):

@Component
public class JerseyConfig extends ResourceConfig {
    public JerseyConfig() {
        register(Endpoint.class);
    }
}

你也可以注册任意数量的,实现ResourceConfigCustomizer的beans来进一步自定义。

所有注册的端点都需注解@Components和HTTP资源annotations(比如@GET):

@Component
@Path("/hello")
public class Endpoint {
    @GET
    public String message() {
        return "Hello";
    }
}

由于Endpoint是一个Spring组件(@Component),所以它的生命周期受Spring管理,你可以使用@Autowired添加依赖,也可以使用@Value注入外部配置。Jersey的servlet会被注册,并默认映射到/*,你可以将@ApplicationPath添加到ResourceConfig来改变该映射。

默认情况下,Jersey将以Servlet的形式注册为一个ServletRegistrationBean类型的@Bean,name为jerseyServletRegistration,该servlet默认会延迟初始化,不过可以通过spring.jersey.servlet.load-on-startup自定义。通过创建相同name的bean,你可以禁用或覆盖框架默认产生的bean。设置spring.jersey.type=filter可以使用Filter的形式代替Servlet,相应的@Bean类型变为jerseyFilterRegistration,该filter有一个@Order属性,你可以通过spring.jersey.filter.order设置。Servlet和Filter注册时都可以使用spring.jersey.init.*定义一个属性集合传递给init参数。

这里有一个Jersey示例,你可以查看如何设置相关事项。

27.3 内嵌servlet容器支持

Spring Boot支持内嵌的Tomcat, Jetty和Undertow服务器,多数开发者只需要使用合适的'Starter'来获取一个完全配置好的实例即可,内嵌服务器默认监听8080端口的HTTP请求。

27.3.1 Servlets, Filters和listeners

使用内嵌servlet容器时,你可以通过使用Spring beans或扫描Servlet组件的方式注册Servlets,Filters及特定Servlet相关的所有listeners(比如HttpSessionListener)。

将Servlets,Filters和listeners注册为Spring beans

所有ServletFilter或Servlet *Listener实例,只要是Spring bean,都会注册到内嵌容器中。如果想在配置期间引用application.properties的属性,这是非常方便的。默认情况下,如果上下文只包含单个Servlet,那它将被映射到/。如果存在多个Servlet beans,那么bean的名称将被用作路径的前缀,过滤器将映射到/*

如果基于约定(convention-based)的映射不够灵活,你可以使用ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean实现完全的控制。

27.3.2 Servlet上下文初始化

内嵌servlet容器不会直接执行Servlet 3.0+的javax.servlet.ServletContainerInitializer接口,或Spring的org.springframework.web.WebApplicationInitializer接口,这样设计的目的是降低war包内运行的第三方库破坏Spring Boot应用的风险。

如果需要在Spring Boot应用中执行servlet上下文初始化,你需要注册一个实现org.springframework.boot.context.embedded.ServletContextInitializer接口的bean。onStartup方法可以获取ServletContext,如果需要的话可以轻松用来适配一个已存在的WebApplicationInitializer

扫描Servlets, Filters和listeners

当使用一个内嵌容器时,通过@ServletComponentScan可以启用对注解@WebServlet@WebFilter@WebListener类的自动注册。

在独立的容器(非内嵌)中@ServletComponentScan不起作用,取为代之的是容器内建的discovery机制。

27.3.3 EmbeddedWebApplicationContext

Spring Boot底层使用一种新的ApplicationContext类型,用于对内嵌servlet容器的支持。EmbeddedWebApplicationContext是一种特殊类型的WebApplicationContext,它通过搜索到的单个EmbeddedServletContainerFactory bean来启动自己,通常TomcatEmbeddedServletContainerFactoryJettyEmbeddedServletContainerFactoryUndertowEmbeddedServletContainerFactory将被自动配置。

你不需要关心这些实现类,大部分应用都能被自动配置,并根据你的行为创建合适的ApplicationContextEmbeddedServletContainerFactory

27.3.4 自定义内嵌servlet容器

常见的Servlet容器配置可以通过Spring Environment进行设置,通常将这些属性定义到application.properties文件中。

常见的服务器配置包括:

  1. 网络设置:监听进入Http请求的端口(server.port),接口绑定地址server.address等。
  2. Session设置:session是否持久化(server.session.persistence),session超时时间(server.session.timeout),session数据存放位置(server.session.store-dir),session-cookie配置(server.session.cookie.*)。
  3. Error管理:错误页面的位置(server.error.path)等。
  4. SSL
  5. HTTP压缩

Spring Boot会尽量暴露常用设置,但这并不总是可能的。对于不可能的情况,可以使用专用的命名空间提供server-specific配置(查看server.tomcatserver.undertow)。例如,可以根据内嵌servlet容器的特性对access logs进行不同的设置。

具体参考ServerProperties

编程方式的自定义

如果需要以编程方式配置内嵌servlet容器,你可以注册一个实现EmbeddedServletContainerCustomizer接口的Spring bean。EmbeddedServletContainerCustomizer能够获取到包含很多自定义setter方法的ConfigurableEmbeddedServletContainer,你可以通过这些setter方法对内嵌容器自定义。

import org.springframework.boot.context.embedded.*;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements EmbeddedServletContainerCustomizer {
    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        container.setPort(9000);
    }
}

直接自定义ConfigurableEmbeddedServletContainer

如果以上自定义手法过于受限,你可以自己注册TomcatEmbeddedServletContainerFactoryJettyEmbeddedServletContainerFactoryUndertowEmbeddedServletContainerFactory

@Bean
public EmbeddedServletContainerFactory servletContainer() {
    TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
    factory.setPort(9000);
    factory.setSessionTimeout(10, TimeUnit.MINUTES);
    factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notfound.html");
    return factory;
}

很多配置选项提供setter方法,有的甚至提供一些受保护的钩子方法以满足你的某些特殊需求,具体参考源码或相关文档。

27.3.5 JSP的限制

当使用内嵌servlet容器运行Spring Boot应用时(并打包成一个可执行的存档archive),容器对JSP的支持有一些限制:

  1. Tomcat只支持war的打包方式,不支持可执行jar。
  2. Jetty只支持war的打包方式。
  3. Undertow不支持JSPs。
  4. 创建的自定义error.jsp页面不会覆盖默认的error handling视图。

这里有个JSP示例,你可以查看如何设置相关事项。